package com.hontech.pastacooking.net

import com.hontech.pastacooking.app.log
import com.hontech.pastacooking.ext.toUInt16
import com.hontech.pastacooking.ext.toUInt32
import com.hontech.pastacooking.utils.tryClose
import java.io.IOException
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.SelectionKey
import java.nio.channels.Selector
import java.nio.channels.SocketChannel

class NIOSocket (private val socket: SocketChannel) : AutoCloseable {

    companion object {

        const val DEFAULT_WRITE_TIMEOUT = 10 * 1000L
        const val DEFAULT_READ_TIMEOUT = 10 * 1000L
        private const val DEFAULT_CONNECT_TIMEOUT = 10 * 1000L

        fun connect(host: String, port: Int, timeout: Long = DEFAULT_CONNECT_TIMEOUT): NIOSocket {
            val socket = SocketChannel.open()

            val selector = try {
                Selector.open()
            } catch (e: Exception) {
                socket.tryClose()
                throw e
            }

            connect(socket, selector, host, port, timeout)

            return NIOSocket(socket)
        }

        private fun connect(socket: SocketChannel, selector: Selector, host: String, port: Int, timeout: Long) {

            try {
                socket.configureBlocking(false)
                socket.connect(InetSocketAddress(host, port))
                socket.register(selector, SelectionKey.OP_CONNECT)

                val n = selector.select(timeout)
                if (n <= 0) {
                    throw IOException("连接超时或者取消")
                }
                selector.selectedKeys().clear()
                val ret = socket.finishConnect()
                if (!ret) {
                    throw IOException("无法连接")
                }

            } catch (e: Exception) {
                socket.tryClose()
                throw e
            } finally {
                selector.tryClose()
            }
        }
    }

    private val writeSelector = Selector.open()
    private val readSelector = Selector.open()

    private val headBuf = ByteBuffer.allocate(8)

    init {
        socket.register(writeSelector, SelectionKey.OP_WRITE)
        socket.register(readSelector, SelectionKey.OP_READ)
    }

    private fun waitReadable(timeout: Long) {
        val n = readSelector.select(timeout)
        if (n <= 0) {
            throw IOException("Read超时或者被取消:$n")
        }
        readSelector.selectedKeys().clear()
    }

    private fun waitWriteable(timeout: Long) {
        val n = writeSelector.select(timeout)
        if (n <= 0) {
            throw IOException("Write超时或者被取消:$n")
        }
        writeSelector.selectedKeys().clear()
    }

    private fun read(buf: ByteBuffer, timeout: Long): ByteArray {
        buf.clear()
        while (buf.remaining() != 0) {
            waitReadable(timeout)
            val n = socket.read(buf)
            if (n <= 0) {
                throw IOException("net closed")
            }
        }
        return buf.array()
    }

    private fun readHead(timeout: Long): ByteArray {
        read(headBuf, timeout)
        return headBuf.array()
    }

    fun read(timeout: Long = DEFAULT_READ_TIMEOUT): ReadFrame {

        val buf = readHead(timeout)
        val head = buf.toUInt16(0)
        if (head != HEAD) {
            throw IOException("invalid head${head}")
        }
        val len = buf.toUInt32(2)
        if (len < 8) {
            throw IOException("invalid size")
        }
        val req = buf.toUInt16(6)
        if (len == 8) {
            return ReadFrame(req, null)
        }
        val size = len - 8
        val bodyBuf = ByteBuffer.allocate(size)
        val body = read(bodyBuf, timeout)
        return ReadFrame(req, body)
    }

    fun write(data: ByteArray, timeout: Long = DEFAULT_WRITE_TIMEOUT) {
        val buf = ByteBuffer.wrap(data)
        while (buf.remaining() != 0) {
            waitWriteable(timeout)
            val n = socket.write(buf)
            if (n <= 0) {
                throw IOException("write 0 ? ")
            }
        }
    }

    override fun close() {
        log("nio socket 关闭")
        socket.tryClose()
        writeSelector.tryClose()
        readSelector.tryClose()
    }
}


















